package main;

/**
 * This is a Finite Differences algorithm and it will update cells in a 2D grid
 * in an almost-in-place manner. It will not duplicate the 2D grid thus leading to
 * memory savings. The basic idea is to progress down along the diagonals (not the main
 * one) and saving only as much context as is required. 
 * @author romain
 *
 */
public class FiniteDiffAlgorithmFloatObj implements IAlgorithm {

	public FiniteDiffAlgorithmFloatObj(TwoDGrid myGrid) {
		// Get the size of the grid to allocate the buffer
		bufferSize = myGrid.getGridSizeX()>myGrid.getGridSizeY()?myGrid.getGridSizeX():myGrid.getGridSizeY();
		buffer = new Float[bufferSize];
		this.myGrid = myGrid;
		relativeError = new Float(0);
	}
	
	public void updateGrid(ICoordinate[] bounds) {
		/* Check bounds. We need at least a 1 border around
		 * what we need to be updating
		 */
		if(bounds.length != 2) 
			throw new java.lang.IllegalArgumentException("Algorithm expecting two bounds");
		
		start = (XYCoordinate)bounds[0];
		end = (XYCoordinate)bounds[1];
		xDiff = end.getPositionX() - start.getPositionX();
		
		if(start.getPositionX() < 1 || start.getPositionY() < 1)
			throw new java.lang.IllegalArgumentException("Algorithm requires border");
		if(end.getPositionX() > myGrid.getGridSizeX() || end.getPositionY() > myGrid.getGridSizeY())
			throw new java.lang.IllegalArgumentException("Algorithm requires border");
		
		/* Reset relative error */
		relativeError = new Float(0);
		
		/* Start updating cells */
		
		/* Start doing the diagonals that touch the side (start.y) */
		for(int startY = start.getPositionY(); startY <= end.getPositionY(); startY++) {
			int y = startY;
			for(int x = start.getPositionX(); y >= start.getPositionY() && x <= end.getPositionX(); x++, y--) {
				updateHelper(new XYCoordinate(x, y));
			}
		}
		/* Now do the other ones */
		for(int startX = start.getPositionX() + 1; startX <= end.getPositionX(); startX++) {
			int x = startX;
			for(int y = end.getPositionY(); y >= start.getPositionY() && x <= end.getPositionX(); x++, y--) {
				updateHelper(new XYCoordinate(x, y));
			}
		}
	}
	
	public Float getRelativeError() {
		return relativeError;
	}

	/* Private */
	protected TwoDGrid myGrid;
	protected Float[] buffer; // Contains buffer values
	protected int bufferSize; // Size of the buffer
	protected Float relativeError; // Contains max error from one step to the next
	protected XYCoordinate start, end; 	// Contains the top-left and bottom-right corners of 
										// the zone to update
	protected int xDiff;
	
	protected int toBufferIndex(int x, int y) {
		/* Gives the index of the value at (x,y) in the buffer
		 * Note that this needs to be used carefully as more than one (x,y)
		 * pair map to the same buffer value. Use this in the correct order
		 */
		
		/* Get which diagnonal we are on in the grid. Well, anti-diagonal */
		int diagNumber = x - start.getPositionX() + y - start.getPositionY();
		
		if(diagNumber <= xDiff) {
			/* In this case, the first element on the diagonal has a y of 0 and x of 
			 * diagNumber. We just look at diagNumber - x to get position */
			return (diagNumber - (x - start.getPositionX()));
		} else {
			/* Here, the buffer is attached to the BOTTOM. So we look off the last position
			 * The last position should go in index xDiff and it has a y of end.getPositionY()
			 */
			return (xDiff - (end.getPositionY() - y ));
		}
	}
	
	protected void updateHelper(XYCoordinate me) {
		XYCoordinate left = (XYCoordinate)myGrid.getNeighbor(me, ICoordinate.DIR_LEFT);
		XYCoordinate right = (XYCoordinate)myGrid.getNeighbor(me, ICoordinate.DIR_RIGHT);
		XYCoordinate bottom = (XYCoordinate)myGrid.getNeighbor(me, ICoordinate.DIR_BOTTOM);
		XYCoordinate top = (XYCoordinate)myGrid.getNeighbor(me, ICoordinate.DIR_TOP);
		
		Float valLeft, valRight, valBottom, valTop;
		
		if(left.getPositionX() < start.getPositionX()) {
			/* Get value from grid */
			valLeft = myGrid.getFloatObjValue(left);
		} else {
			/* Get value from buffer */
			valLeft = buffer[toBufferIndex(left.getPositionX(), left.getPositionY())];
		}
		
		if(top.getPositionY() < start.getPositionY()) {
			/* From grid */
			valTop = myGrid.getFloatObjValue(top);
		} else {
			/* From buffer */
			valTop = buffer[toBufferIndex(top.getPositionX(), top.getPositionY())];
		}
		
		/* Right and bottom are always read from the grid */
		valRight = myGrid.getFloatObjValue(right);
		valBottom = myGrid.getFloatObjValue(bottom);
		
		/* Update cell and error */
		Float oldValue, newValue;
		oldValue = myGrid.getFloatObjValue(me);
		newValue = new Float(0.25*(valTop.floatValue() + valLeft.floatValue() + valRight.floatValue() + valBottom.floatValue()));
		
		/* Calculate error */
		if(oldValue.floatValue() != 0)
			relativeError = new Float(java.lang.Math.max(relativeError.floatValue(), java.lang.Math.abs((oldValue.floatValue() - newValue.floatValue())/oldValue.floatValue())));
		else
			relativeError = new Float(java.lang.Math.max(relativeError.floatValue(), java.lang.Math.abs((oldValue.floatValue() - newValue.floatValue()))));
		/* Update the buffer value */
		buffer[toBufferIndex(me.getPositionX(), me.getPositionY())] = oldValue;
		
		/* Update the grid */
		myGrid.setFloatObjValue(me, newValue);
	}
}
